/* * OverviewController.java - Copyright(c) 2013 Joe Pasqua * Provided under the MIT License. See the LICENSE file for details. * Created: Jul 22, 2013 */ package org.noroomattheinn.visibletesla; import java.util.HashMap; import java.util.Map; import java.util.concurrent.Callable; import javafx.application.Platform; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.fxml.FXML; import javafx.scene.control.Button; import javafx.scene.control.Dialogs; import javafx.scene.control.Dialogs.DialogOptions; import javafx.scene.control.Label; import javafx.scene.control.TextArea; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.AnchorPane; import org.apache.commons.lang3.StringUtils; import org.noroomattheinn.tesla.ChargeState; import org.noroomattheinn.tesla.Options; import org.noroomattheinn.tesla.Result; import org.noroomattheinn.tesla.StreamState; import org.noroomattheinn.tesla.Vehicle; import org.noroomattheinn.tesla.Vehicle.PanoCommand; import org.noroomattheinn.tesla.VehicleState; import org.noroomattheinn.utils.Utils; public class OverviewController extends BaseController { /*------------------------------------------------------------------------------ * * Constants and Enums * *----------------------------------------------------------------------------*/ private static final String ToggleChoiceKey = "DISP_VIN"; private enum ToggleDisplayChoice { VIN, SW, FW }; private static final int nToggleChoices = ToggleDisplayChoice.values().length; /*------------------------------------------------------------------------------ * * Internal State * *----------------------------------------------------------------------------*/ private ToggleDisplayChoice toggleChoice; /*------------------------------------------------------------------------------ * * UI Elements * *----------------------------------------------------------------------------*/ // Lock Status Images @FXML private ImageView lockedImg; @FXML private ImageView unlockedImg; // // Car Images (and Labels) // @FXML private ImageView bodyImg; // Wheels @FXML private ImageView silver21Front, silver21Rear; @FXML private ImageView darkRimFront, darkRimRear; @FXML private ImageView nineteenRimFront, nineteenRimRear; @FXML private ImageView aeroFront, aeroRear; @FXML private ImageView cycloneFront, cycloneRear; @FXML private ImageView darkCycloneFront, darkCycloneRear; private Map<Options.WheelType,Options.WheelType> wheelEquivs = new HashMap<>(); private Map<Options.WheelType,ImageView[]> wheelImages = new HashMap<>(); @FXML private ImageView seatsTanImg, seatsGrayImg; // Driver Side @FXML private ImageView dfOpenImg, dfClosedImg; @FXML private ImageView drOpenImg, drClosedImg; // Passenger Side @FXML private ImageView pfOpenImg; @FXML private ImageView prOpenImg; // Trunk/Frunk @FXML private ImageView ftClosedImg, ftOpenImg; @FXML private ImageView rtOpenImg, rtClosedImg; @FXML private ImageView spoilerClosedImg, spoilerOpenImg; // Roof Images (Solipath+Pano: Open, Closepath+Vent) @FXML private ImageView blackRoofImg, solidRoofImg; @FXML private ImageView panoClosedImg, panoVentImg, panoOpenImg; @FXML private Label panoPercent; // Charging related images @FXML private ImageView chargeCableImg, portClosedImg, portOpenImg, greenGlowImage; // Other Labels @FXML private Label shiftStateLabel; @FXML private Label rangeLabel; @FXML private Label odometerLabel; @FXML private Button vinButton; // Emblem Images @FXML private ImageView s60Img, s85Img, p85Img, p85pImg, p85dImg, s85dImg, s70dImg; // // Controls // @FXML private Button lockButton; @FXML private Button closePanoButton, ventPanoButton, openPanoButton; /*------------------------------------------------------------------------------ * * UI Action Handlers * *----------------------------------------------------------------------------*/ @FXML void lockButtonHandler(ActionEvent event) { final Button source = (Button)event.getSource(); issueCommand(new Callable<Result>() { @Override public Result call() { Result r = vtVehicle.getVehicle().setLockState(source == lockButton); updateStateLater(Vehicle.StateType.Vehicle, 3 * 1000); return r; } }, (source == lockButton) ? "Lock" : "Unlock"); } @FXML void panoButtonHandler(ActionEvent event) { Button source = (Button)event.getSource(); final PanoCommand cmd = source == ventPanoButton ? PanoCommand.vent : ((source == openPanoButton) ? PanoCommand.open : PanoCommand.close); issueCommand(new Callable<Result>() { @Override public Result call() { Result r = vtVehicle.getVehicle().setPano(cmd); updateStateLater(Vehicle.StateType.Vehicle, 5 * 1000); return r; } }, "Move Pano"); } @FXML void detailsButtonHandler(ActionEvent event) { AnchorPane pane = new AnchorPane(); VehicleState car = vtVehicle.vehicleState.get(); String info = vtVehicle.getVehicle().toString() + "\nFirmware Version: " + car.version + "\nRemote Start Enabled: " + vtVehicle.getVehicle().remoteStartEnabled() + "\nCalendar Enabled: " + vtVehicle.getVehicle().calendarEnabled() + "\nNotifications Enabled: " + vtVehicle.getVehicle().notificationsEnabled() + "\n--------------------------------------------" + "\nLow level information: " + vtVehicle.getVehicle().getUnderlyingValues() + "\nVehicle UUID: " + vtVehicle.getVehicle().getUUID() + "\nApp UUID: " + app.getAppID() + "\n"; TextArea t = new TextArea(info); pane.getChildren().add(t); Dialogs.showCustomDialog( app.stage, pane, "Detailed Vehicle Description", "Details", DialogOptions.OK, null); } /*------------------------------------------------------------------------------ * * Methods overridden from BaseController * *----------------------------------------------------------------------------*/ @Override protected void fxInitialize() { odometerLabel.setVisible(true); wheelImages.put(Options.WheelType.WT19, new ImageView[] {nineteenRimFront, nineteenRimRear}); wheelEquivs.put(Options.WheelType.WT1P, Options.WheelType.WT19); wheelEquivs.put(Options.WheelType.WTX1, Options.WheelType.WT19); wheelImages.put(Options.WheelType.WTAE, new ImageView[] {aeroFront, aeroRear}); wheelEquivs.put(Options.WheelType.WTAP, Options.WheelType.WTAE); wheelImages.put(Options.WheelType.WTTB, new ImageView[] {cycloneFront, cycloneRear}); wheelEquivs.put(Options.WheelType.WTTP, Options.WheelType.WTTB); wheelImages.put(Options.WheelType.WTTG, new ImageView[] {darkCycloneFront, darkCycloneRear}); wheelEquivs.put(Options.WheelType.WTGP, Options.WheelType.WTTG); wheelImages.put(Options.WheelType.WT21, new ImageView[] {silver21Front, silver21Rear}); wheelEquivs.put(Options.WheelType.WT2E, Options.WheelType.WT21); wheelEquivs.put(Options.WheelType.WTSS, Options.WheelType.WT21); wheelImages.put(Options.WheelType.WTSP, new ImageView[] {darkRimFront, darkRimRear}); wheelEquivs.put(Options.WheelType.WTSE, Options.WheelType.WTSP); wheelEquivs.put(Options.WheelType.WTSG, Options.WheelType.WTSP); } /** * Refresh the state either because the user requested it or because the * auto-refresh interval has passed. We always update the car and * charge. Getting the odometer reading can be more burdensome because * it has to be done through the streaming API. We only do that every 3rd * time refresh is invoked, or if the user pressed the refresh button. * This keeps down our request rate to the tesla servers. * */ @Override protected void refresh() { updateState(Vehicle.StateType.Vehicle); updateState(Vehicle.StateType.Charge); } @Override protected void initializeState() { final Vehicle v = vtVehicle.getVehicle(); getAppropriateImages(v); toggleChoice = this.storedToggleChoice(); prefs.overrides.color.addListener(new ChangeListener<String>() { @Override public void changed( ObservableValue<? extends String> ov, String t, String t1) { getAppropriateImages(v); } }); prefs.overrides.doColor.addListener(new ChangeListener<Boolean>() { @Override public void changed( ObservableValue<? extends Boolean> ov, Boolean t, Boolean t1) { getAppropriateImages(v); } }); vtVehicle.vehicleState.addTracker(new Runnable() { @Override public void run() { Platform.runLater(new Runnable() { @Override public void run() { updateVehicleState(); } }); } }); vtVehicle.chargeState.addTracker(new Runnable() { @Override public void run() { Platform.runLater(new Runnable() { @Override public void run() { updateChargePort(); updateRange(); } }); } }); vtVehicle.streamState.addTracker(new Runnable() { @Override public void run() { Platform.runLater(new Runnable() { @Override public void run() { updateOdometer(); updateShiftState(); } }); } }); updateOdometer(); // Show at least an old reading to start vtData.produceStream(false); // Update it at some point updateWheelView(); // Make sure we display the right wheels from the get-go updateRoofView(); // Make sure we display the right roof from the get-go reflectVINOrFirmware(); vinButton.setOnAction(new EventHandler<ActionEvent>() { @Override public void handle(ActionEvent event) { cycleToggleChoice(); reflectVINOrFirmware(); } }); } @Override protected void activateTab() { } /*------------------------------------------------------------------------------ * * Methods to Reflect the overall state of the vehicle * *----------------------------------------------------------------------------*/ private void updateVehicleState() { updateWheelView(); updateRoofView(); updateDoorView(); updateOdometer(); updateEmblem(); updateSeats(); updateRange(); updateShiftState(); } private void updateRange() { ChargeState cs = vtVehicle.chargeState.get(); double range = 0; String rangeType = prefs.overviewRange.get(); switch (rangeType) { case "Estimated": range = cs.estimatedRange; break; case "Ideal": range = cs.idealRange; break; case "Rated": range = cs.range; break; } range = vtVehicle.inProperUnits(range); String units = vtVehicle.unitType() == Utils.UnitType.Imperial ? "mi" : "km"; rangeLabel.setText(String.format("%s Range: %3.1f %s", rangeType, range, units)); } private void updateShiftState() { StreamState snapshot = vtVehicle.streamState.get(); if (snapshot == null) return; shiftStateLabel.setText(snapshot.shiftState()); } private void updateDoorView() { VehicleState car = vtVehicle.vehicleState.get(); boolean rtOpen = car.isRTOpen; // Show the open/closed state of the doors and trunks setOptionState(car.isFTOpen, ftOpenImg, ftClosedImg); setOptionState(rtOpen, rtOpenImg, rtClosedImg); setOptionState(car.isDFOpen, dfOpenImg, dfClosedImg); setOptionState(car.isPFOpen, pfOpenImg, null); setOptionState(car.isDROpen, drOpenImg, drClosedImg); setOptionState(car.isPROpen, prOpenImg, null); setOptionState(car.locked, lockedImg, unlockedImg); spoilerOpenImg.setVisible(false); spoilerClosedImg.setVisible(false); if (vtVehicle.vehicleState.get().hasSpoiler) { setOptionState(rtOpen, spoilerOpenImg, spoilerClosedImg); } } private void updateRoofView() { Options.RoofType type = vtVehicle.roofType(); boolean hasPano = (type == Options.RoofType.RFPO); // Start with all images set to invisible, then turn on the one right one panoOpenImg.setVisible(false); panoClosedImg.setVisible(false); panoVentImg.setVisible(false); solidRoofImg.setVisible(false); blackRoofImg.setVisible(false); // Only show the pano controls and percent if we have a pano roof closePanoButton.setVisible(hasPano); ventPanoButton.setVisible(hasPano); openPanoButton.setVisible(hasPano); panoPercent.setVisible(hasPano); if (hasPano) updatePanoView(); else setOptionState(type == Options.RoofType.RFBC, solidRoofImg, blackRoofImg); } private void updatePanoView() { VehicleState car = vtVehicle.vehicleState.get(); int pct = car.panoPercent; if (pct == 0) panoClosedImg.setVisible(true); else if (pct > 0 && pct < 90) panoVentImg.setVisible(true); else panoOpenImg.setVisible(true); panoPercent.setText(String.valueOf(pct) + " %"); } private void updateWheelView() { updateImages(vtVehicle.wheelType(), wheelImages, wheelEquivs); } private void updateChargePort() { ChargeState charge = vtVehicle.chargeState.get(); boolean connected = charge.connectedToCharger(); boolean chargePortDoorOpen = (charge.chargePortOpen || connected); setOptionState(chargePortDoorOpen, portOpenImg, portClosedImg); chargeCableImg.setVisible(connected); greenGlowImage.setVisible(charge.chargingState == ChargeState.Status.Charging); } private void updateSeats() { seatsGrayImg.setVisible(false); seatsTanImg.setVisible(false); switch (vtVehicle.getVehicle().getOptions().seatType().getColor()) { case Gray: case White: seatsGrayImg.setVisible(true); break; case Tan: seatsTanImg.setVisible(true); break; case Black: // Do nothing, the base image is black default: break; } } private void updateEmblem() { s60Img.setVisible(false); s85Img.setVisible(false); p85Img.setVisible(false); p85pImg.setVisible(false); s70dImg.setVisible(false); s85dImg.setVisible(false); p85dImg.setVisible(false); switch (vtVehicle.model()) { // RWD Standard Models case S60: s60Img.setVisible(true); break; case S85: s85Img.setVisible(true); break; // RWD Performance Models case P85: p85Img.setVisible(true); break; case P85Plus: p85pImg.setVisible(true); break; // AWD Standard & Performance Models case S70D: s70dImg.setVisible(true); break; case S85D: s85dImg.setVisible(true); break; case P85D: p85dImg.setVisible(true); break; default: s85Img.setVisible(true); break; } } private void updateOdometer() { double odometerReading; if (vtVehicle.streamState.get().valid) { odometerReading = vtVehicle.streamState.get().odometer; prefs.storage().putDouble(vinKey("odometer"), odometerReading); } else { odometerReading = prefs.storage().getDouble(vinKey("odometer"), 0); } boolean useMiles = vtVehicle.unitType() == Utils.UnitType.Imperial; String units = useMiles ? "mi" : "km"; odometerReading *= useMiles ? 1.0 : Utils.KilometersPerMile; odometerLabel.setText(String.format("Odometer: %.1f %s", odometerReading, units)); } private void reflectVINOrFirmware() { VehicleState car = vtVehicle.vehicleState.get(); String text; switch (toggleChoice) { case SW: text = "v" + Firmware.getSoftwareVersion(car.version); break; case FW: text = "FW: " + car.version; break; case VIN: default: text = "VIN " + StringUtils.right(vtVehicle.getVehicle().getVIN(), 6); break; } vinButton.setText(text); } private ToggleDisplayChoice storedToggleChoice() { String currentAsString = prefs.storage().get("DISP_VIN", ToggleDisplayChoice.VIN.name()); return ToggleDisplayChoice.valueOf(currentAsString); } private void cycleToggleChoice() { int nextIndex = (toggleChoice.ordinal()+1) % nToggleChoices; toggleChoice = ToggleDisplayChoice.values()[nextIndex]; prefs.storage().put(ToggleChoiceKey, toggleChoice.name()); } /*------------------------------------------------------------------------------ * * State and Methods for locating the right images based on vehicle parameters * *----------------------------------------------------------------------------*/ // This Map maps from a PaintColor to a directory name which holds the // images for that color. As new colors are added by Tesla, the map // must be udated (as must the PaintColor enum). private static final Map<Options.PaintColor,String> colorToDirectory = new HashMap<>(); static { colorToDirectory.put(Options.PaintColor.PBCW, "COLOR_white/"); colorToDirectory.put(Options.PaintColor.PBSB, "COLOR_black/"); colorToDirectory.put(Options.PaintColor.PMAB, "COLOR_brown/"); colorToDirectory.put(Options.PaintColor.PMMB, "COLOR_blue/"); colorToDirectory.put(Options.PaintColor.PMSG, "COLOR_green/"); colorToDirectory.put(Options.PaintColor.PMSS, "COLOR_silver/"); colorToDirectory.put(Options.PaintColor.PMTG, "COLOR_gray/"); colorToDirectory.put(Options.PaintColor.PPMR, "COLOR_newred/"); colorToDirectory.put(Options.PaintColor.PPSR, "COLOR_red/"); colorToDirectory.put(Options.PaintColor.PPSW, "COLOR_pearl/"); colorToDirectory.put(Options.PaintColor.PMNG, "COLOR_steelgrey/"); colorToDirectory.put(Options.PaintColor.PPTI, "COLOR_titanium/"); colorToDirectory.put(Options.PaintColor.PMBL, "COLOR_black/"); colorToDirectory.put(Options.PaintColor.PPSB, "COLOR_oceanblue/"); colorToDirectory.put(Options.PaintColor.Unknown, "COLOR_white/"); } // Where the images are stored relative to the classpath private static final String ImagePrefix = "org/noroomattheinn/TeslaResources/"; // Replace the images that were selected by default with images for the actual color private void getAppropriateImages(Vehicle v) { Options.PaintColor c = vtVehicle.paintColor(); ClassLoader cl = getClass().getClassLoader(); String colorDirectory = colorToDirectory.get(c); String path = ImagePrefix + colorDirectory; if (v.getOptions().driveSide() == Options.DriveSide.DRLH) bodyImg.setImage(new Image(cl.getResourceAsStream(path+"body@2x.png"))); else bodyImg.setImage(new Image(cl.getResourceAsStream(path+"body_RHD@2x.png"))); dfOpenImg.setImage(new Image(cl.getResourceAsStream(path+"left_front_open@2x.png"))); dfClosedImg.setImage(new Image(cl.getResourceAsStream(path+"left_front_closed@2x.png"))); drOpenImg.setImage(new Image(cl.getResourceAsStream(path+"left_rear_open@2x.png"))); drClosedImg.setImage(new Image(cl.getResourceAsStream(path+"left_rear_closed@2x.png"))); pfOpenImg.setImage(new Image(cl.getResourceAsStream(path+"right_front_open@2x.png"))); prOpenImg.setImage(new Image(cl.getResourceAsStream(path+"right_rear_open@2x.png"))); ftClosedImg.setImage(new Image(cl.getResourceAsStream(path+"frunk_closed@2x.png"))); ftOpenImg.setImage(new Image(cl.getResourceAsStream(path+"frunk_open@2x.png"))); rtOpenImg.setImage(new Image(cl.getResourceAsStream(path+"trunk_open@2x.png"))); rtClosedImg.setImage(new Image(cl.getResourceAsStream(path+"trunk_closed@2x.png"))); solidRoofImg.setImage(new Image(cl.getResourceAsStream(path+"roof@2x.png"))); } }